package com.atguigu.boot_redis02.controller;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author gcq
 * @Create 2020-12-11
 * <p>
 * 总结：1、syhchronized单机版本OK，上分布式
 *      2、nginx分布式，单机锁不行了
 *      3、取消单机锁，上分布式锁
 *      4、只加了锁、没有释放锁、出现了异常、可能也无法释放锁、必须要在代码层面finally释放锁
 *      5、宕机了，部署了微服务的代码根本就没有走到finally这块，没有办法解锁，这个key没有被删除，需要有lockKey指定过期时间
 *      6、为redis的分布式锁key，增加过期时间，此外，setnx+过期时间设置必须同一行
 *      7、必须规定自己只能删除自己的锁，不能删除别人的锁，防止张冠李戴，1删2，2删3
 *      8、redis集群环境下的，我们自己写的一个redis分布式锁不是特别好，所以直接上RedLock之Redisson落地实现
 *
 * ===============================注意：最终版本9.1为主===============================
 * 其他版本可用来测试
 */
@RestController
public class GoodController {

    private String REDIS_LOCK = "atguiguLock";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    private String GOODS = "goods:001";

    @Autowired
    private Redisson redisson;

    /**
     * 1.0版本
     * 问题：没有加锁，并发下数字不对，出现超卖现象
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods1")
    public String buy_goods1() throws Exception {

        String result = stringRedisTemplate.opsForValue().get(GOODS);

        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        // 库存大于0
        if (goodsNumber > 0) {
            // 减库存
            int realNumber = goodsNumber - 1;
            // 重新设置回去
            stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
            System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
            return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
        } else {
            System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
        }
        return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;

    }

    /**
     * 2.0版本
     * 新增功能：synchronized代码块
     * 出现问题：加锁后使用 jmeter 100个线程一秒钟压测，出现一个商品买了两次/商品超卖问题未解决
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods2")
    public String buy_goods2() throws Exception {
        synchronized (this) {
            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        }
    }

    /**
     * 3.0版本 使用分布式锁setnx 保证高并发情况下，商品不会出现超卖
     * 出现问题：如果程序出现异常，可能无法释放锁，必须要在代码层面finally解锁
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods3")
    public String buy_goods3() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);

        if (!flag) {
            return "抢锁失败";
        }

        String result = stringRedisTemplate.opsForValue().get(GOODS);

        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        // 库存大于0
        if (goodsNumber > 0) {
            // 减库存
            int realNumber = goodsNumber - 1;
            // 重新设置回去
            stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
            System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
            stringRedisTemplate.delete(REDIS_LOCK);
            return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
        } else {
            System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
        }
        return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;

    }

    /**
     * 4.0版本
     * 新增功能：代码层面finally层面解锁
     * 出现问题：部署微服务的jar包宕机了，程序没有走到finally这块，没办法解锁，key没有删除
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods4")
    public String buy_goods4() throws Exception {

        try {
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }

    /**
     * 5.0版本
     * 新增功能：设置锁的过期时间
     * 出现问题：高并发下，创建锁和设置锁过期时间应该保证原子性
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods5")
    public String buy_goods5() throws Exception {

        try {
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
            // 设置10秒过期时间
            stringRedisTemplate.expire(REDIS_LOCK, 10, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }

    /**
     * 6.0版本
     * 新增功能：设置key+过期时间具备原子操作
     * 出现问题：业务执行时间过长，超过了锁的过期时间，正好执行完业务准备删除锁，
     * 这时候别的线程进入，刚好就把别人的锁进行了删除，张冠李戴，删了别人的锁
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods6")
    public String buy_goods6() throws Exception {

        try {
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }

    /**
     * 7.0版本
     * 新增功能：删除锁进行判断，只能自己删除自己的锁，不能动别人的
     * 出现问题：finally判断 + 解锁 不是原子性的
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods7")
    public String buy_goods7() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {


            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            // 判断加锁和解锁有可能不是同一个客户端
            if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)) {
                // 若在此时，这把锁突然不是这个客户端的，则会误解锁
                stringRedisTemplate.delete(REDIS_LOCK);
            }
        }
    }


    /**
     * 8.1版本
     * 新增功能：使用redis事务进行删除锁
     * 出现问题：官网推荐使用lua脚本进行删除
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods8_1")
    public String buy_goods8_1() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {

            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            while (true) {
                // 监控key
                stringRedisTemplate.watch(REDIS_LOCK);
                if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
                    // 设置为true，使用MULTI、EXEC来跟踪操作
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    // 开启事务
                    stringRedisTemplate.multi();
                    // 删除
                    stringRedisTemplate.delete(REDIS_LOCK);
                    // 提交
                    List<Object> list = stringRedisTemplate.exec();
                    // 为空继承进行删除
                    if (list == null) {
                        continue;
                    }
                }
                // 取消监控
                stringRedisTemplate.unwatch();
                break;
            }
        }
    }

    /**
     * 8.2版本
     * 新增功能：使用lua脚本进行删除
     * 出现问题：使用redis实现分布式锁太过复杂，
     * 官方建议使用redisson
     * 官网文档地址：<a href="http://www.redis.cn/topics/distlock.html"></a>
     * redissonGitHub中文文档地址：<a href="https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95"></a>
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods8_2")
    public String buy_goods8_2() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败";
            }

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            Jedis jedis = new Jedis();

            String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                    "then " +
                    "return redis.call('del',KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";
            try {
                Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                // 为1删除成功
                if ("1".equals(o.toString())) {
                    System.out.println("------del redis lock ok");
                } else {
                    // 为0代表lua脚本删除
                    System.out.println("------del redis lock error");
                }
            } finally {
                if (null != jedis) {
                    jedis.close();
                }
            }
        }
    }

    /**
     * 9.0版本
     * 新增功能：使用Redisson实现分布式锁
     * 出现问题：可能会出现 attempt to unlock lock,not locked by current thread by node id
     * 当前线程和解锁线程不是一个，finally代码块中解锁需要判断
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods9")
    public String buy_goods9() throws Exception {

        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();
        try {

            String result = stringRedisTemplate.opsForValue().get(GOODS);

            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            redissonLock.unlock();
        }
    }

    /**
     * 9.1版本
     * 新增功能：集群情况下，我们自己写的也不ok，所以直接上RedLock之Redisson落地实现
     * 出现问题：暂无
     *
     * @return
     * @throws Exception
     */
    @GetMapping("/buy_goods9_1")
    public String buy_goods9_1() throws Exception {
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();
        try {
            String result = stringRedisTemplate.opsForValue().get(GOODS);
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            // 库存大于0
            if (goodsNumber > 0) {
                // 减库存
                int realNumber = goodsNumber - 1;
                // 重新设置回去
                stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(realNumber));
                System.out.println("成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort);
                return "成功买到商品，库存还剩下：" + realNumber + "件" + "\t 服务提供端口 " + serverPort;
            } else {
                System.out.println("商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort);
            }
            return "商品已经售完/活动结束/调用超时，欢迎下次光临" + "\t 服务提供端口 " + serverPort;
        } finally {
            // 是否被任何线程锁定
            if (redissonLock.isLocked()) {
                // 是否是当前线程
                if (redissonLock.isHeldByCurrentThread()) {
                    redissonLock.unlock();
                }
            }

        }
    }
}